<!DOCTYPE html><html lang="[&quot;zh-Hans&quot;,&quot;zh-CN&quot;,&quot;default&quot;]"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><meta name="description" content="react_ssr笔记"><meta name="keywords" content="ssr,react"><meta name="author" content="wenmu"><meta name="copyright" content="wenmu"><title>react_ssr笔记 | 温木的博客</title><link rel="shortcut icon" href="/melody-favicon.ico"><link rel="stylesheet" href="/css/index.css?version=1.6.1"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@latest/css/font-awesome.min.css?version=1.6.1"><link rel="dns-prefetch" href="https://cdn.staticfile.org"><link rel="dns-prefetch" href="https://cdn.bootcss.com"><link rel="dns-prefetch" href="https://creativecommons.org"><link rel="dns-prefetch" href="http://ta.qq.com"><script>(function() {
   var hm = document.createElement("script");
   hm.src = "https://tajs.qq.com/stats?sId=&lt;script type=&quot;text/javascript&quot; src=&quot;http://tajs.qq.com/stats?sId=66523541&quot; charset=&quot;UTF-8&quot;&gt;&lt;/script&gt;";
   var s = document.getElementsByTagName("script")[0];
   s.parentNode.insertBefore(hm, s);
 })();</script><script>var GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: undefined,
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  }
} </script><meta name="generator" content="Hexo 4.2.0"></head><body><i class="fa fa-arrow-right" id="toggle-sidebar" aria-hidden="true"></i><div id="sidebar"><div class="toggle-sidebar-info text-center"><span data-toggle="切换文章详情">切换站点概览</span><hr></div><div class="sidebar-toc"><div class="sidebar-toc__title">目录</div><div class="sidebar-toc__progress"><span class="progress-notice">你已经读了</span><span class="progress-num">0</span><span class="progress-percentage">%</span><div class="sidebar-toc__progress-bar"></div></div><div class="sidebar-toc__content"><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#react-实现-ssr-和原理讲解"><span class="toc-number">1.</span> <span class="toc-text">react 实现 ssr 和原理讲解</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#服务端渲染"><span class="toc-number">1.1.</span> <span class="toc-text">服务端渲染</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#在服务端写-react-代码"><span class="toc-number">1.2.</span> <span class="toc-text">在服务端写 react 代码</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#webpack-node-externals"><span class="toc-number">1.2.1.</span> <span class="toc-text">webpack-node-externals</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#使用-renderToString-服务端渲染-react-组件"><span class="toc-number">1.2.2.</span> <span class="toc-text">使用 renderToString 服务端渲染 react 组件</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#前端渲染-CSR-和服务端渲染-SSR-的优缺点"><span class="toc-number">1.2.3.</span> <span class="toc-text">前端渲染(CSR)和服务端渲染(SSR)的优缺点</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#webapck-server-js"><span class="toc-number">1.2.4.</span> <span class="toc-text">webapck.server.js</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#自动打包和服务自动启动"><span class="toc-number">1.2.5.</span> <span class="toc-text">自动打包和服务自动启动</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#使用-npm-run-all-进一步提升开发效率"><span class="toc-number">1.2.6.</span> <span class="toc-text">使用 npm-run-all 进一步提升开发效率</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#同构"><span class="toc-number">1.2.7.</span> <span class="toc-text">同构</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#让-react-组件在客户端再运行一次"><span class="toc-number">1.2.8.</span> <span class="toc-text">让 react 组件在客户端再运行一次</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#webpack-复用公共部分"><span class="toc-number">1.2.9.</span> <span class="toc-text">webpack 复用公共部分</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#服务端路由"><span class="toc-number">1.2.10.</span> <span class="toc-text">服务端路由</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#改造客户端和服务端使用路由进行渲染"><span class="toc-number">1.2.11.</span> <span class="toc-text">改造客户端和服务端使用路由进行渲染</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#同构-redux"><span class="toc-number">1.2.12.</span> <span class="toc-text">同构 redux</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#抽离公共store"><span class="toc-number">1.2.13.</span> <span class="toc-text">抽离公共store</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#服务端渲染时加载-store-数据"><span class="toc-number">1.2.14.</span> <span class="toc-text">服务端渲染时加载 store 数据</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#同构存在的问题！！！"><span class="toc-number">1.2.15.</span> <span class="toc-text">同构存在的问题！！！</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#让客户端的请求都通过node中间件"><span class="toc-number">1.2.16.</span> <span class="toc-text">让客户端的请求都通过node中间件</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#node代理时没有携带解决cookie问题"><span class="toc-number">1.2.17.</span> <span class="toc-text">node代理时没有携带解决cookie问题</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#共用部分实现，比如Header"><span class="toc-number">1.2.18.</span> <span class="toc-text">共用部分实现，比如Header</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#通过axios给请求添加公共参数"><span class="toc-number">1.2.19.</span> <span class="toc-text">通过axios给请求添加公共参数</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#404-页面"><span class="toc-number">1.2.20.</span> <span class="toc-text">404 页面</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#借助router的context来返回-404-状态码"><span class="toc-number">1.2.21.</span> <span class="toc-text">借助router的context来返回 404 状态码</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#服务端重定向"><span class="toc-number">1.2.22.</span> <span class="toc-text">服务端重定向</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#解决请求失败的问题"><span class="toc-number">1.2.23.</span> <span class="toc-text">解决请求失败的问题</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#服务端渲染css配置"><span class="toc-number">1.2.24.</span> <span class="toc-text">服务端渲染css配置</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#使用高阶组件解决需要在每个组件中添加css字符串的问题"><span class="toc-number">1.2.25.</span> <span class="toc-text">使用高阶组件解决需要在每个组件中添加css字符串的问题</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#SEO"><span class="toc-number">1.2.26.</span> <span class="toc-text">SEO</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#使用react-helmet定制-title-和-Description"><span class="toc-number">1.2.27.</span> <span class="toc-text">使用react-helmet定制 title 和 Description</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#预渲染"><span class="toc-number">1.2.28.</span> <span class="toc-text">预渲染</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#搭建预渲染服务器"><span class="toc-number">1.2.29.</span> <span class="toc-text">搭建预渲染服务器</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#PreRender-原理"><span class="toc-number">1.2.30.</span> <span class="toc-text">PreRender 原理</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#如何判断是否是蜘蛛在访问"><span class="toc-number">1.2.31.</span> <span class="toc-text">如何判断是否是蜘蛛在访问</span></a></li></ol></li></ol></li></ol></div></div><div class="author-info hide"><div class="author-info__avatar text-center"><img src="https://i.loli.net/2020/02/27/xwaBoOj158MfNyq.jpg"></div><div class="author-info__name text-center">wenmu</div><div class="author-info__description text-center">记录一些在工作中或学习新知识时学到的一些知识点；正所谓好记性不如一个烂笔头，温故而知新！</div><hr><div class="author-info-articles"><a class="author-info-articles__archives article-meta" href="/archives"><span class="pull-left">文章</span><span class="pull-right">115</span></a><a class="author-info-articles__tags article-meta" href="/tags"><span class="pull-left">标签</span><span class="pull-right">52</span></a><a class="author-info-articles__categories article-meta" href="/categories"><span class="pull-left">分类</span><span class="pull-right">30</span></a></div></div></div><div id="content-outer"><div class="no-bg" id="top-container"><div id="page-header"><span class="pull-left"> <a id="site-name" href="/">温木的博客</a></span><i class="fa fa-bars toggle-menu pull-right" aria-hidden="true"></i><span class="pull-right menus"><a class="site-page" href="/">首页</a><a class="site-page" href="/archives">文章</a><a class="site-page" href="/tags">标签</a><a class="site-page" href="/categories">分类</a></span></div><div id="post-info"><div id="post-title">react_ssr笔记</div><div id="post-meta"><time class="post-meta__date"><i class="fa fa-calendar" aria-hidden="true"></i> 2020-01-09</time><span class="post-meta__separator">|</span><i class="fa fa-inbox post-meta__icon" aria-hidden="true"></i><a class="post-meta__categories" href="/categories/react/">react</a></div></div></div><div class="layout" id="content-inner"><article id="post"><div class="article-container" id="post-content"><h1 id="react-实现-ssr-和原理讲解"><a href="#react-实现-ssr-和原理讲解" class="headerlink" title="react 实现 ssr 和原理讲解"></a>react 实现 ssr 和原理讲解</h1><p>使用 react 实现 ssr，并讲解实现 ssr 的原理。</p>
<h2 id="服务端渲染"><a href="#服务端渲染" class="headerlink" title="服务端渲染"></a>服务端渲染</h2><p>服务端渲染很简单，就是在服务端生成 html 代码字符串，然后返回给浏览器。<br>所以对前端来说就是一个请求。<br>本课程使用 express 启动服务。</p>
<a id="more"></a>

<h2 id="在服务端写-react-代码"><a href="#在服务端写-react-代码" class="headerlink" title="在服务端写 react 代码"></a>在服务端写 react 代码</h2><p>首先同样需要先安装 react 的包，babel 进行转义，不然因为写的是 node 代码，连 import 都不能直接使用，使用了 babel 这些，自然就需要使用 webpack 进行打包转义了。</p>
<blockquote>
<p><code>webpack</code>不仅可以打包前端代码，服务端代码也可以，只是在<code>webpack</code>的配置文件中，需要添加<code>target:&#39;node&#39;</code>，告诉 webpack 这是服务端代码，不然像<code>path</code>这样的工具包会被打包进代码中，在服务端<code>path</code>工具全局环境中的，是可以直接使用的。</p>
</blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i webpack webpack-cli react react-dom babel-loader babel-core babel-preset-react@latest babel-preset-stage-0 babel-preset-env --save</span><br></pre></td></tr></table></figure>

<p>babel 是个很好的转义工具，但是各种转义都需要安装对应的 preset，比如上面的 react，stage-0 语法等。</p>
<h3 id="webpack-node-externals"><a href="#webpack-node-externals" class="headerlink" title="webpack-node-externals"></a>webpack-node-externals</h3><p>虽然上面通过设置<code>target</code>可以避免 node 环境中已有包不会被打包到代码中，但是<code>node_modules</code>中安装的第三方包比如 <code>express</code> 还是被打包到引用它的文件中。这样会导致 express 被重复打包。<br>使用 webpack-node-externals 可以避免这种情况发生，这会保证各个文件中保持<code>require(&#39;express&#39;)</code>的写法。(有点像前端优化的动态链接库，自我感觉)</p>
<blockquote>
<p>️⚠️ 不做这个处理，打包会报警告！</p>
</blockquote>
<h3 id="使用-renderToString-服务端渲染-react-组件"><a href="#使用-renderToString-服务端渲染-react-组件" class="headerlink" title="使用 renderToString 服务端渲染 react 组件"></a>使用 renderToString 服务端渲染 react 组件</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; renderToString &#125; <span class="keyword">from</span> <span class="string">"react-dom/server"</span>;</span><br><span class="line"><span class="keyword">import</span> Home <span class="keyword">from</span> <span class="string">"./Home"</span>;</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">renderToString(<span class="xml"><span class="tag">&lt;<span class="name">Home</span> /&gt;</span>)</span></span><br></pre></td></tr></table></figure>

<h3 id="前端渲染-CSR-和服务端渲染-SSR-的优缺点"><a href="#前端渲染-CSR-和服务端渲染-SSR-的优缺点" class="headerlink" title="前端渲染(CSR)和服务端渲染(SSR)的优缺点"></a>前端渲染(CSR)和服务端渲染(SSR)的优缺点</h3><blockquote>
<p>SSR 有利于 SEO，但是对服务端性能消耗比较大。</p>
</blockquote>
<h3 id="webapck-server-js"><a href="#webapck-server-js" class="headerlink" title="webapck.server.js"></a>webapck.server.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">"path"</span>);</span><br><span class="line"><span class="keyword">const</span> nodeExternals = <span class="built_in">require</span>(<span class="string">"webpack-node-externals"</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  target: <span class="string">"node"</span>, <span class="comment">// 指定运行环境是服务端环境</span></span><br><span class="line">  mode: <span class="string">"development"</span>, <span class="comment">// 指定开发</span></span><br><span class="line">  entry: <span class="string">"./src/index.js"</span>,</span><br><span class="line">  output: &#123;</span><br><span class="line">    filename: <span class="string">"bundle.js"</span>,</span><br><span class="line">    path: path.resolve(__dirname, <span class="string">"build"</span>),</span><br><span class="line">  &#125;,</span><br><span class="line">  externals: [nodeExternals()],</span><br><span class="line">  <span class="built_in">module</span>: &#123;</span><br><span class="line">    rules: [</span><br><span class="line">      &#123;</span><br><span class="line">        test: <span class="regexp">/\.js?$/</span>,</span><br><span class="line">        loader: <span class="string">"babel-loader"</span>,</span><br><span class="line">        exclude: [<span class="string">"/node_modules/"</span>],</span><br><span class="line">        options: &#123;</span><br><span class="line">          <span class="comment">// 指定需要转义的内容，需要安装对应的babel插件。</span></span><br><span class="line">          presets: [</span><br><span class="line">            <span class="string">"react"</span>,</span><br><span class="line">            <span class="string">"stage-0"</span>,</span><br><span class="line">            <span class="comment">/* 如何根据环境进行适配 */</span></span><br><span class="line">            [</span><br><span class="line">              <span class="string">"env"</span>,</span><br><span class="line">              &#123;</span><br><span class="line">                targets: &#123;</span><br><span class="line">                  browsers: [<span class="string">"last 2 versions"</span>], <span class="comment">// 在打包编译的过程中会去兼容所有浏览器的最后两个版本，当然可以设置3个版等等。</span></span><br><span class="line">                &#125;,</span><br><span class="line">              &#125;,</span><br><span class="line">            ],</span><br><span class="line">          ],</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;,</span><br><span class="line">    ],</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="自动打包和服务自动启动"><a href="#自动打包和服务自动启动" class="headerlink" title="自动打包和服务自动启动"></a>自动打包和服务自动启动</h3><p>打包和启动是两个命令，前端由 webpack-dev-server 可以帮忙解决这个问题，但是服务端，目前还没有这样的工具，只能把代码用 webpack 打包，然后用 node 启动，命令如下：</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">"scripts": &#123;</span><br><span class="line">  "start": "node ./build/bundle.js",</span><br><span class="line">  "build": "webpack --config webpack.server.js"</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure>

<p>解决上面的问题也很简单，<code>webpack</code>自带了检测功能，增加<code>--watch</code>参数即可；<br><code>node</code>需要借助一个工具<code>nodemon</code>,<code>nodemon</code>启动的<code>node</code>服务可以自动检测启动文件所在目录是否发生变化，如果发生变化则自动重启服务。<br><code>nodemon</code>也可以设置监控多个文件夹。</p>
<blockquote>
<p><code>nodemon</code>还有一个同类工具叫<code>supervisor</code></p>
</blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i nodemon -g</span><br></pre></td></tr></table></figure>

<p>修改后启动配置</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">"scripts": &#123;</span><br><span class="line">  "start": "nodemon --watch build ./build/bundle.js",</span><br><span class="line">  "build": "webpack --config webpack.server.js --watch"</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure>

<h3 id="使用-npm-run-all-进一步提升开发效率"><a href="#使用-npm-run-all-进一步提升开发效率" class="headerlink" title="使用 npm-run-all 进一步提升开发效率"></a>使用 npm-run-all 进一步提升开发效率</h3><p>上面的配置虽然实现了自动打包，服务自动启动的功能，但是还是需要打开两个命令窗口，还是很麻烦。<br>使用 npm-run-all 就可以只打开一个窗口就可以了。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo npm i npm-run-all -g</span><br></pre></td></tr></table></figure>

<p>配置如下：</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">"scripts": &#123;</span><br><span class="line">  "dev": "npm-run-all --parallel dev:**",</span><br><span class="line">  "dev:start": "nodemon --watch build ./build/bundle.js",</span><br><span class="line">  "dev:build": "webpack --config webpack.server.js --watch"</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure>

<blockquote>
<ul>
<li><code>parallel</code> 指定并行执行</li>
<li><code>dev:**</code>指定并行执行以”<code>dev:</code>“开头的所有 npm 命令</li>
</ul>
</blockquote>
<p>这样执行<code>npm run dev</code>两个服务就都启动了。</p>
<h3 id="同构"><a href="#同构" class="headerlink" title="同构"></a>同构</h3><p>服务端渲染时，如果在 react 组件上增加事件，当渲染到浏览器上时，事件是无法被绑定到组件上的。<br>这该怎么解决呢？这就需要使用<strong>同构</strong>。</p>
<blockquote>
<p>同构：就是一套 react 代码，在服务端执行一次，然后在客户端再执行一次。</p>
</blockquote>
<h3 id="让-react-组件在客户端再运行一次"><a href="#让-react-组件在客户端再运行一次" class="headerlink" title="让 react 组件在客户端再运行一次"></a>让 react 组件在客户端再运行一次</h3><p>这就比较简单了，这就是正常的前端 react 开发，把 react 组件和挂载打包后，在入口 html 引用就可以了。打包后的文件就是 js 吗，让打包的组件在客户端再挂载一次。<br>不同的是，这次挂载用的不是 ReactDom.render 而是 ReactDom.hydrate()方法。因为 react 组件已经被服务端渲染了一遍，做同构，就不要使用 render，而是使用 dydrate</p>
<blockquote>
<p>服务端渲染挂载组件两边不要有文本节点，也就是把 react 组件转换成字符串 A 后，传给 html 字符串 B 时，A 要和它的父元素在一行，不要有空格、换行，挨着写就行了。</p>
</blockquote>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div id=<span class="string">"root"</span>&gt;$&#123;renderToString(<span class="xml"><span class="tag">&lt;<span class="name">Home</span> /&gt;</span>)&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br></pre></td></tr></table></figure>

<ol>
<li>前端执行的 js 包入口</li>
</ol>
<p>// src/client/index.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> ReactDom <span class="keyword">from</span> <span class="string">"react-dom"</span>;</span><br><span class="line"><span class="keyword">import</span> Home <span class="keyword">from</span> <span class="string">"../Home"</span>;</span><br><span class="line"></span><br><span class="line">ReactDom.hydrate(<span class="xml"><span class="tag">&lt;<span class="name">Home</span> /&gt;</span>, document.getElementById("root"));</span></span><br></pre></td></tr></table></figure>

<ol start="2">
<li>配置 webpack 打包前端 js 包</li>
</ol>
<p>把<code>webpack.server.js</code>中<code>node</code>的相关配置删除即可。把<code>js文件</code>打包到<code>public</code>文件夹中。</p>
<ol start="3">
<li>增加打包命令</li>
</ol>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">"dev:build:client": "webpack --config webpack.client.js --watch"</span><br></pre></td></tr></table></figure>

<ol start="4">
<li>在首页 html 中引入 client 打包的静态 js</li>
</ol>
<p>使用<code>express</code>的<code>static</code>中间件，把<code>public</code>文件夹设置成静态资源请求目录。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 指定请求的静态资源都去public文件夹中获取</span></span><br><span class="line"><span class="comment"> * 并且不用写public路径，如下面的index</span></span><br><span class="line"><span class="comment"> * */</span></span><br><span class="line">app.use(express.static(<span class="string">"public"</span>));</span><br></pre></td></tr></table></figure>

<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">app.get(<span class="string">"/"</span>, (req, res) =&gt; &#123;</span><br><span class="line">  res.send(<span class="string">`&lt;html&gt;</span></span><br><span class="line"><span class="string">        &lt;head&gt;</span></span><br><span class="line"><span class="string">        &lt;title&gt;ssr&lt;/title&gt;</span></span><br><span class="line"><span class="string">        &lt;/head&gt;</span></span><br><span class="line"><span class="string">        &lt;body&gt;</span></span><br><span class="line"><span class="string">            &lt;div id="root"&gt;<span class="subst">$&#123;renderToString(&lt;Home <span class="regexp">/&gt;)&#125;&lt;/</span>div&gt;</span></span></span><br><span class="line"><span class="string"><span class="subst">        &lt;<span class="regexp">/body&gt;</span></span></span></span><br><span class="line"><span class="string"><span class="subst"><span class="regexp">        &lt;script src="/i</span>ndex.js<span class="string">"&gt;&lt;/script&gt;</span></span></span></span><br><span class="line"><span class="string"><span class="subst"><span class="string">    &lt;/html&gt;`);</span></span></span></span><br><span class="line"><span class="string"><span class="subst"><span class="string">&#125;);</span></span></span></span><br></pre></td></tr></table></figure>

<p>经过上述的修改，按钮的事件就被绑定到元素上了，点击就有反应了。</p>
<blockquote>
<p>react 说白了就是 dom 操作，所以客户端再绑定一次，元素就被 dom 动态的又创建一次，事件什么的当然有了。</p>
</blockquote>
<h3 id="webpack-复用公共部分"><a href="#webpack-复用公共部分" class="headerlink" title="webpack 复用公共部分"></a>webpack 复用公共部分</h3><p>由于<code>webpack.server.js</code>和<code>webpack.client.js</code>大部分都相同，所以可以抽出公共部分，两个“继承”后各自扩展就行了；使用<code>webpack-merge</code>可以达到这个目的。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i webpack-merge</span><br></pre></td></tr></table></figure>

<h3 id="服务端路由"><a href="#服务端路由" class="headerlink" title="服务端路由"></a>服务端路由</h3><p>路由同样需要进行同构操作，但服务端路由不是使用<code>BrowserRouter</code>,而是需要使用<code>StaticRouter</code></p>
<p>首先安装 react 的 router 包<code>react-router-dom</code>，<code>BrowserRouter</code>和<code>StaticRouter</code>是这个包中的两个模块</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i react-router-dom --save</span><br></pre></td></tr></table></figure>

<h3 id="改造客户端和服务端使用路由进行渲染"><a href="#改造客户端和服务端使用路由进行渲染" class="headerlink" title="改造客户端和服务端使用路由进行渲染"></a>改造客户端和服务端使用路由进行渲染</h3><p>前端使用<code>BrowserRouter</code>进行路由,改造<code>client</code>中的<code>index.js</code></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> ReactDom <span class="keyword">from</span> <span class="string">"react-dom"</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; BrowserRouter &#125; <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"><span class="keyword">import</span> Routes <span class="keyword">from</span> <span class="string">"../Routes"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> App = <span class="xml"><span class="tag">&lt;<span class="name">BrowserRouter</span>&gt;</span>&#123;Routes&#125;<span class="tag">&lt;/<span class="name">BrowserRouter</span>&gt;</span></span>;</span><br><span class="line"></span><br><span class="line">ReactDom.hydrate(<span class="xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span>, document.getElementById("root"));</span></span><br></pre></td></tr></table></figure>

<p>服务端代码改造</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;div id=<span class="string">"root"</span>&gt;</span><br><span class="line">  $</span><br><span class="line">  &#123;renderToString(</span><br><span class="line">    &lt;StaticRouter location=&#123;req.path&#125; context=&#123;&#123;&#125;&#125;&gt;</span><br><span class="line">      &#123;Routes&#125;</span><br><span class="line">    &lt;<span class="regexp">/StaticRouter&gt;,</span></span><br><span class="line"><span class="regexp">  )&#125;</span></span><br><span class="line"><span class="regexp">&lt;/</span>div&gt;</span><br></pre></td></tr></table></figure>

<p><code>StaticRouter</code>和<code>BrowserRouter</code>不同，它不能自动感知 url 中的路由信息，所以需要设置<code>location</code>属性来告知服务端的 router 当前的路径是什么。</p>
<p><strong>多个路由时的处理</strong><br>因为 react 是单页面，所以服务端的路由需要都指定到入口文件上。比如增加了一个 login 页面，不能增加 get(/login)，把入口的路由改成全部(*)即可。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">app.get(<span class="string">"*"</span>, (req, res) =&gt; &#123;</span><br><span class="line">  res.send(<span class="string">`&lt;html&gt;</span></span><br><span class="line"><span class="string">        &lt;head&gt;</span></span><br><span class="line"><span class="string">        &lt;title&gt;ssr&lt;/title&gt;</span></span><br><span class="line"><span class="string">        &lt;/head&gt;</span></span><br><span class="line"><span class="string">        &lt;body&gt;</span></span><br><span class="line"><span class="string">        &lt;h1&gt;hello,this is a ssr content.&lt;/h1&gt;</span></span><br><span class="line"><span class="string">            &lt;h1&gt;下面是home组件的内容&lt;/h1&gt;</span></span><br><span class="line"><span class="string">            &lt;div id="root"&gt;<span class="subst">$&#123;renderToString(</span></span></span><br><span class="line"><span class="string"><span class="subst">              &lt;StaticRouter location=&#123;req.path&#125;</span> context=&#123;&#123;&#125;&#125;&gt;</span></span><br><span class="line"><span class="string">                &#123;Routes&#125;</span></span><br><span class="line"><span class="string">              &lt;/StaticRouter&gt;,</span></span><br><span class="line"><span class="string">            )&#125;&lt;/div&gt;</span></span><br><span class="line"><span class="string">        &lt;/body&gt;</span></span><br><span class="line"><span class="string">        &lt;script src="/index.js"&gt;&lt;/script&gt;</span></span><br><span class="line"><span class="string">    &lt;/html&gt;`</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h3 id="同构-redux"><a href="#同构-redux" class="headerlink" title="同构 redux"></a>同构 redux</h3><p>加上 redux 比较简单，和正常的开发一样就行。客户端和服务端同样都做相同处理，不然报错。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i react-redux redux redux-thunk</span><br></pre></td></tr></table></figure>

<p>相关代码:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> reducer = <span class="function">(<span class="params">state = &#123; name: <span class="string">"温木"</span> &#125;, action</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> state;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> store = createStore(reducer, applyMiddleware(thunk));</span><br><span class="line"></span><br><span class="line">&lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">  &lt;StaticRouter location=&#123;req.path&#125; context=&#123;&#123;&#125;&#125;&gt;</span><br><span class="line">    &#123;Routes&#125;</span><br><span class="line">  &lt;<span class="regexp">/StaticRouter&gt;</span></span><br><span class="line"><span class="regexp">&lt;/</span>Provider&gt;;</span><br></pre></td></tr></table></figure>

<h3 id="抽离公共store"><a href="#抽离公共store" class="headerlink" title="抽离公共store"></a>抽离公共<code>store</code></h3><p>由于服务端和客户端的 store 是一样的，因此可以把公共的部分抽成公共的模块， 但是需要注意的是，<code>createStore</code>应该每次调动时都生成新的 store，不然所有的服务就会调用的是同一个 store</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createStore, applyMiddleware &#125; <span class="keyword">from</span> <span class="string">"redux"</span>;</span><br><span class="line"><span class="keyword">import</span> thunk <span class="keyword">from</span> <span class="string">"redux-thunk"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> reducer = <span class="function">(<span class="params">state = &#123; name: <span class="string">"温木"</span> &#125;, action</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> state;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">getStore</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> createStore(reducer, applyMiddleware(thunk));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 下面是错误代码，下面的写法会让所有的服务共用一个store</span></span><br><span class="line"><span class="comment">// const store = createStore(reducer, applyMiddleware(thunk));</span></span><br><span class="line"><span class="comment">// export default store;</span></span><br></pre></td></tr></table></figure>

<blockquote>
<p><code>Warning: Expected server HTML to contain a matching &lt;div&gt; in &lt;div&gt;.</code><br>引起这种 bug 我遇到有两种：</p>
<ol>
<li>只在客户端渲染使用了路由，服务端渲染没有使用，服务端加上即可</li>
<li>服务端在渲染是多嵌套了 div，比如我遇到的就是把 html 字符串封装成了方法 render，但是原来的 html 字符串忘记删除，在 html 字符串中又调用了 render，所以导致了这个警告。</li>
</ol>
</blockquote>
<h3 id="服务端渲染时加载-store-数据"><a href="#服务端渲染时加载-store-数据" class="headerlink" title="服务端渲染时加载 store 数据"></a>服务端渲染时加载 store 数据</h3><p>通常情况下，我们请求数据都在<code>componentDidMount</code>声明周期中进行，但是声明周期函数<code>componentDidMount</code>(<code>useEffect</code>也一样)在服务端渲染时是不执行的，只有在客户端渲染时才会触发。因此<code>React</code>为服务端渲染提供了专门的数据加载方法。</p>
<p><a href="https://reacttraining.com/react-router/web/guides/server-rendering" target="_blank" rel="noopener">详情参考<code>react router</code>的<code>Server Rendering</code>的<code>Data Loading</code>部分</a></p>
<ol>
<li>首先每个组件需要增加一个静态的<code>loadData</code>函数，用于服务端渲染时获取组件的异步数据</li>
<li>给路由增加<code>loadData</code>属性，官网推荐用一个数组 map 输出</li>
<li>服务端渲染时，绑定 store 之前，使用<code>matchPath</code>向 store 中填充数据，不过 matchPath 只能匹配一层路由，多级路由需要使用<code>react-router-config</code>的<code>matchRoutes</code>,</li>
<li>由于获取数据是异步，所以需要数据请求结束后借助<code>Promise.all</code>的回调返回<code>html</code>字符串（注意:<code>action</code>和<code>loadData</code>中要把<code>promise</code>对象透传出去，把<code>promise</code>对象<code>return</code>出去）</li>
</ol>
<blockquote>
<p>node 不能使用 fetch<br>fetch 在 node 中不能使用，所以有服务端渲染时最好使用<code>axios</code>发请求。<br><strong>切记<code>loadData</code>要把结果<code>return</code></strong></p>
</blockquote>
<h3 id="同构存在的问题！！！"><a href="#同构存在的问题！！！" class="headerlink" title="同构存在的问题！！！"></a>同构存在的问题！！！</h3><p>由于同构服务端渲染一遍，客户端又渲染了一遍，那就有一个问题了，当服务端把数据已经渲染了，异步数据也拿到了，由于客户端还会再执行一遍，那数据还会再执行并渲染一般，那前端由的延迟不是还是能感觉到吗，这不是脱裤放屁了，当禁用掉<code>js</code>,由于前端<code>js</code>无法执行。渲染异步请求的数据的部分会是空白；（当然 SEO 不影响，因为数据确实已经在页面了）</p>
<p><strong>如何解决这个问题呢？</strong><br>这需要用到数据的注水和脱水。</p>
<blockquote>
<p>说白了就是当服务端已经拿到的<code>state</code>数据放到<code>window</code>的全局变量里面，当前端再创建<code>store</code>的时候，取出来全局中存储的<code>state</code>数据传给<code>store</code>当做默认值，这就没抖动了。<br>都是笨方法新概念，听着挺高大上</p>
</blockquote>
<p>创建 store 时候，服务端和客户端分开使用两个方法就行了。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">getStore</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> createStore(reducer, applyMiddleware(thunk));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 脱水</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">getClientStore</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> defaultState = <span class="built_in">window</span>.context.state;</span><br><span class="line">  <span class="keyword">return</span> createStore(reducer, defaultState, applyMiddleware(thunk));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>那客户端不发出请求不行了吗？<br>不行，因为服务端渲染只是渲染首屏 A，当手动输入页面路径首先访问 B 时，再回到 A 这是就无法获取 A 的数据了，所以客户端的请求不能省略；为了避免客户端重复发出请求，可以加个判断，有数据就不请求。</p>
<h3 id="让客户端的请求都通过node中间件"><a href="#让客户端的请求都通过node中间件" class="headerlink" title="让客户端的请求都通过node中间件"></a>让客户端的请求都通过<code>node</code>中间件</h3><p>到目前为止，<code>action</code>发出的请求是直接访问服务器的，既然搭建了 node 中间件，所有的请求应该都通过中间件发出去，客户端不应该直接和服务端进行交互。<br>实现也简单，因为现在有了 node 服务，如果发出的请求是相对路径，那么都会被 node 服务接收，我们只要制定好规则，把获取数据的请求都转到数据服务器就行了，说白了就是加个代理。<br>代理需要用到<code>express-http-proxy</code>,通过这个插件可以轻松的把请求代理到指定的服务器。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i express-http-proxy</span><br></pre></td></tr></table></figure>

<p>我这里做的约定是，所有数据的请求都以<code>/api</code>开头。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 设置代理，代理只能处理客户端发出的请求，服务端的请求不走这块，需要额外处理</span></span><br><span class="line">app.use(</span><br><span class="line">  <span class="string">"/api"</span>,</span><br><span class="line">  proxy(<span class="string">"https://www.easy-mock.com"</span>, &#123;</span><br><span class="line">    https: <span class="literal">true</span>,</span><br><span class="line">    proxyReqPathResolver: <span class="function"><span class="keyword">function</span>(<span class="params">req</span>) </span>&#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="string">`/mock/5d7073309fb10a1868876f68/react_ssr/api<span class="subst">$&#123;req.url&#125;</span>`</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;),</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>⚠️ 服务端渲染的时候有问题。</p>
<blockquote>
<p>比如我当时报错信息是：Error: connect ECONNREFUSED 127.0.0.1:80</p>
</blockquote>
<p>因为<code>action</code>中的请求链接现在写的相对路径(比如:<code>/api/list.json</code>)，当浏览器发出请求的时候，浏览器会想网站挂载的服务器发出请求，也就是使用网站的域名和端口。<br>但是服务端在发出请求的时候，由于这本身就在服务端，所以对服务器来说，这是请求根目录下的<code>api</code>目录下<code>list.json</code>文件，由于根目录下没有这个文件，所以就报错了，请求接口同样的道理。<br>要解决这个问题也很简单，就是大家都能想到的最笨的方法。<br>做个判断处理，服务端发起的请求就把请求路径写成数据服务器的绝对路径，比如：<code>https://www.baidu.com/data/api/list.json</code>,如果是客户端发起的请求就写相对路径<code>/api/list.json</code>;</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> url = <span class="string">`/api/list`</span>;</span><br><span class="line"><span class="keyword">if</span> (isServer) &#123;</span><br><span class="line">  url = <span class="string">"https://www.easy-mock.com/mock/5d7073309fb10a1868876f68/react_ssr/api/list"</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这样做很丑陋，如果这样处理，首屏上用到的异步数据 action 都要这么处理，太 low 了。使用<code>axios</code>的<code>instance</code>可以改善这个问题(其实差别不大,还是需要判断)。</p>
<p>原理就是创建两个<code>axios</code>的实例，一个客户端的，一个服务端的，把上面的 url 换成两个实例就行了。<code>axios</code>的实例可以指定发出请求的前缀内容，所以方法中的<code>url</code>可以写成相对路径，拼接工作嫁给了<code>axios</code>的实例来完成。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// client</span></span><br><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">"axios"</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> axios.create(&#123;</span><br><span class="line">  baseURL: <span class="string">"/"</span>,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// server</span></span><br><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">"axios"</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> axios.create(&#123;</span><br><span class="line">  baseURL: <span class="string">"https://www.easy-mock.com/mock/5d7073309fb10a1868876f68/react_ssr"</span>,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用</span></span><br><span class="line"><span class="keyword">let</span> request = isServer ? ServerAxios : ClientAxios;</span><br><span class="line">request.get(<span class="string">"/api/list"</span>);</span><br></pre></td></tr></table></figure>

<p><strong>不使用判断更优雅的解决</strong><br>由于客户端渲染和服务端渲染分别使用各自的<code>stroe</code>，因此可以在创建<code>store</code>的时候，绑定<code>axios</code>的实例，这样就不用传参判断是否是客户端还是服务端请求了。<br>给<code>store</code>绑定<code>axios</code>实例，需要用到<code>redux-thunk</code>的<code>withExtraArgument</code>方法，这个方法允许给<code>thunk</code>传递一个参数，这个参数在异步调用的时候可以使用。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// store绑定axios实例</span></span><br><span class="line"><span class="keyword">import</span> ClientAxios <span class="keyword">from</span> <span class="string">"../client/request"</span>;</span><br><span class="line"><span class="keyword">import</span> ServerAxios <span class="keyword">from</span> <span class="string">"../server/request"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">getStore</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> createStore(reducer, applyMiddleware(thunk.withExtraArgument(ClientAxios)));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">getClientStore</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> defaultState = <span class="built_in">window</span>.context.state;</span><br><span class="line">  <span class="keyword">return</span> createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(ServerAxios)));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// action中调用</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">fetchHomeList</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 第三个参数就是绑定的axios实例</span></span><br><span class="line">  <span class="keyword">return</span> <span class="function">(<span class="params">dispatch, getState, axiosInstance</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> axiosInstance.get(<span class="string">"/api/list"</span>).then(<span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; data &#125; = response;</span><br><span class="line">      dispatch(setHome(data.data));</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>有没有想过为什么客户端和服务端渲染不用同一个 store 呢？，这样还不用注水和脱水呢！<br>因为客户端和服务端运行环境不同，一个运行客户端也就是浏览器中，一个运行在 node 环境中，变量怎么可能共享呢，哈哈 😀</p>
</blockquote>
<h3 id="node代理时没有携带解决cookie问题"><a href="#node代理时没有携带解决cookie问题" class="headerlink" title="node代理时没有携带解决cookie问题"></a><code>node</code>代理时没有携带解决<code>cookie</code>问题</h3><p>服务端代理请求时，携带上客户单请求上的<code>cookie</code>就行了;更新创建<code>axios</code>实例的地方</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">"axios"</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> req =&gt;</span><br><span class="line">  axios.create(&#123;</span><br><span class="line">    baseURL: <span class="string">"https://www.easy-mock.com/mock/5d7073309fb10a1868876f68/react_ssr"</span>,</span><br><span class="line">    headers: &#123;</span><br><span class="line">      cookie: req.get(<span class="string">"cookie"</span>) || <span class="string">""</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure>

<h3 id="共用部分实现，比如Header"><a href="#共用部分实现，比如Header" class="headerlink" title="共用部分实现，比如Header"></a>共用部分实现，比如<code>Header</code></h3><p>项目中总会有很多页面的共用部分，比如<code>header</code>,<code>footer</code>等等，这些如何像使用模板那样在一个地方写，而不需要在每个页面中都写一遍呢？<br>使用<code>router</code><strong>多级路由</strong>就可以轻松解决这个问题。</p>
<ol>
<li>写一个共用组件,如<code>App.js</code>，组件中放入公共部分，比如<code>Header</code>、<code>Footer</code>、<code>Menu</code>等等。</li>
<li>在<code>App.js</code>中增加各个组件的路由,但是为了能显示多级路由的内容，需要用到<code>react-router-config</code>的<code>renderRoutes</code></li>
<li>把<code>App</code>组件作为路由对象的根对象，子集路由就是要在内容区域显示的各个子组件了。</li>
</ol>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 路由对象</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  &#123;</span><br><span class="line">    path: <span class="string">"/"</span>,</span><br><span class="line">    component: App,</span><br><span class="line">    key: <span class="string">"app"</span>,</span><br><span class="line">    routes: [</span><br><span class="line">      &#123;</span><br><span class="line">        path: <span class="string">"/"</span>,</span><br><span class="line">        component: Home,</span><br><span class="line">        loadData: Home.loadData,</span><br><span class="line">        exact: <span class="literal">true</span>,</span><br><span class="line">        key: <span class="string">"home"</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        path: <span class="string">"/login"</span>,</span><br><span class="line">        component: Login,</span><br><span class="line">        exact: <span class="literal">true</span>,</span><br><span class="line">        key: <span class="string">"login"</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">    ],</span><br><span class="line">  &#125;,</span><br><span class="line">];</span><br><span class="line"><span class="comment">// 服务端渲染注册路由的地方</span></span><br><span class="line">&lt;StaticRouter location=&#123;req.path&#125; context=&#123;&#123;&#125;&#125;&gt;</span><br><span class="line">  &lt;div&gt;&#123;renderRoutes(routes)&#125;&lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">&lt;/</span>StaticRouter&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 显示路由的地方,App组件</span></span><br><span class="line"><span class="keyword">import</span> routes <span class="keyword">from</span> <span class="string">"./Routes"</span>;</span><br><span class="line"><span class="comment">//....省略部分</span></span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line">  &lt;div&gt;</span><br><span class="line">    &lt;Header /&gt;</span><br><span class="line">    &#123;renderRoutes(routes[<span class="number">0</span>].routes)&#125;</span><br><span class="line">  &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">);</span></span><br><span class="line"><span class="regexp">/</span><span class="regexp">/ 客户端注册路由的地方</span></span><br><span class="line"><span class="regexp">&lt;BrowserRouter&gt;</span></span><br><span class="line"><span class="regexp">  &lt;div&gt;&#123;renderRoutes(routes)&#125;&lt;/</span>div&gt;</span><br><span class="line">&lt;<span class="regexp">/BrowserRouter&gt;;</span></span><br></pre></td></tr></table></figure>

<p>由于一级路由渲染完组件后，路由会把路由对象的信息传递给组件，因此 App 公共组件不需要专门引入定义的 routes，在 props 中可以直接获取到</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> App = memo(<span class="function"><span class="params">props</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;Header /&gt;</span><br><span class="line">      &#123;renderRoutes(props.route.routes)&#125;</span><br><span class="line">    &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;);</span></span><br></pre></td></tr></table></figure>

<h3 id="通过axios给请求添加公共参数"><a href="#通过axios给请求添加公共参数" class="headerlink" title="通过axios给请求添加公共参数"></a>通过<code>axios</code>给请求添加公共参数</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> axios.create(&#123;</span><br><span class="line">  baseURL: <span class="string">"/"</span>,</span><br><span class="line">  params: &#123; <span class="attr">secret</span>: <span class="string">"abc"</span> &#125;,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> req =&gt;</span><br><span class="line">  axios.create(&#123;</span><br><span class="line">    baseURL: <span class="string">"https://www.fastmock.site/mock/0564485c18844ec86d683c16b922e8ab/ssr"</span>,</span><br><span class="line">    headers: &#123;</span><br><span class="line">      cookie: req.get(<span class="string">"cookie"</span>) || <span class="string">"cookie"</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    params: &#123; <span class="attr">secret</span>: <span class="string">"abc"</span> &#125;,</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure>

<h3 id="404-页面"><a href="#404-页面" class="headerlink" title="404 页面"></a>404 页面</h3><p>当访问的路由不存在时，我们需要显示<code>404</code>页面，这实现其实很简单。<br>写一个 404 的页面，然后在配置路由的地方配置下就行，但是配置只需要一个<code>component</code>属性即可。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  path: <span class="string">"/"</span>,</span><br><span class="line">  component: App,</span><br><span class="line">  loadData: App.loadData,</span><br><span class="line">  key: <span class="string">"app"</span>,</span><br><span class="line">  routes: [</span><br><span class="line">    &#123;</span><br><span class="line">      component: NotFound,   <span class="comment">// 这行就是404页面</span></span><br><span class="line">    &#125;,</span><br><span class="line">  ],</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure>

<p>当访问路由中没有的路由时，会自动跳转到 404 页面。</p>
<h3 id="借助router的context来返回-404-状态码"><a href="#借助router的context来返回-404-状态码" class="headerlink" title="借助router的context来返回 404 状态码"></a>借助<code>router</code>的<code>context</code>来返回 404 状态码</h3><p>上面的 404 页面配置完后，虽然其作用了，但是 http 状态码是 200，为了更真实的显示，我们应该让找不到路由时 http 的状态码是<code>404</code></p>
<p>给<code>StaticRouter</code>组件传递<code>context</code>属性，当返回 html 字符串时判断 context 对象是否被标记访问的是 404 页面，比如添加了 notFound 属性，如果是，则设置状态码为 404</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Promise</span>.all(promises).then(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 用于404页面时增加标记用</span></span><br><span class="line">  <span class="keyword">const</span> context = &#123;&#125;;</span><br><span class="line">  <span class="keyword">const</span> html = render(req, store, routes, context);</span><br><span class="line">  <span class="keyword">if</span> (context.noFound) &#123;</span><br><span class="line">    res.status(<span class="number">404</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  res.send(html);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h3 id="服务端重定向"><a href="#服务端重定向" class="headerlink" title="服务端重定向"></a>服务端重定向</h3><p>前面我们已经做了，当退出登录时，使用<code>Redirect</code>调回到首页的功能，但是<code>Redirect</code>仅限于前端重定向，不能做服务端重定向；<br>在非登录状态下，如果我们直接手动访问其他页面，其实服务端是已经访问了的，只是客户端帮我们跳转到了首页。</p>
<p>服务端该如何重定向呢？<br>这仍需要借助<code>router</code>的<code>context</code>对象，当我们有重定向时，<code>context</code>会被填充一些属性，通过打印 context 对象，可以发现，填充的属性有：</p>
<ul>
<li><code>action</code> 字符串，描述操作类型，重定向就是<code>REPLACE</code></li>
<li><code>location</code> 一个对象，描述重定向前的数据</li>
<li><code>url</code> 字符串，描述重定向后的<code>url</code></li>
</ul>
<p>所以在服务端渲染时，可以像处理 404 时那样，判断 context 中的 action，如果是<code>REPLACE</code>则跳转到<code>url</code>指定的路径</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (context.action === <span class="string">"REPLACE"</span>) &#123;</span><br><span class="line">  res.redirect(<span class="number">301</span>, context.url);</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (context.noFound) &#123;</span><br><span class="line">  res.status(<span class="number">404</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="解决请求失败的问题"><a href="#解决请求失败的问题" class="headerlink" title="解决请求失败的问题"></a>解决请求失败的问题</h3><p>在循环组件并加载数据的地方再嵌套一层 promise,无论组件中加载数据的部分是成功还是失败都把结果传递下去，这样只有失败的组件不显示，成功的组件仍显示。<br>并且当有多个请求时，如果第一个失败了，后面的数据还没返回也不影响，因为 <code>catch</code> 也让触发的 <code>resolve</code></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">matchedRoutes.forEach(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (item.route.loadData) &#123;</span><br><span class="line">    <span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      item.route</span><br><span class="line">        .loadData(store)</span><br><span class="line">        .then(resolve)</span><br><span class="line">        .catch(resolve);</span><br><span class="line">    &#125;);</span><br><span class="line">    promises.push(promise);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h3 id="服务端渲染css配置"><a href="#服务端渲染css配置" class="headerlink" title="服务端渲染css配置"></a>服务端渲染<code>css</code>配置</h3><p>服务端渲染<code>css</code>需要使用到<code>isomorphic-style-loader</code></p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install isomorphic-style-loader --save-dev</span><br></pre></td></tr></table></figure>

<p>然后把<code>style-loader</code>改成<code>isomorphic-style-loader</code>。</p>
<p>配置完后，下面的处理就和 404 页面的处理类似，当是服务端渲染时，就把<code>css</code>字符串添加到<code>router</code>的<code>context</code>对象中，然后在服务端渲染那块，从<code>context</code>中拿出<code>css</code>字符串，拼接到<code>head</code>中即可。<br>如果获取<code>css</code>字符串呢？<br>首先把 <code>css</code> 引入方式配置成模块引入的方式。<code>isomorphic-style-loader</code>提供了<code>_getCss()</code>的方法可以获取到 <code>css</code> 字符串。<br>这样禁用掉 <code>js</code> 后，样式仍能直接有效。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 判断是否是服务端渲染</span></span><br><span class="line"><span class="keyword">if</span> (staticContext) &#123;</span><br><span class="line">  staticContext.css = styles._getCss();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>但是这样有很多问题：</p>
<ol>
<li>因为需要在每个组件中把<code>css</code>添加到<code>context</code>对象中，所以 css 会被后面的覆盖</li>
<li>不在路由中的组件没有<code>context</code>属性</li>
</ol>
<p>解答：</p>
<ol>
<li>css 属性用数组类型</li>
<li>手动把<code>context</code>对象传递过去。因为<code>App</code>组件是在路由中的，所以可以在<code>App</code>中把<code>conterxt</code>传递给<code>header</code>组件中</li>
</ol>
<h3 id="使用高阶组件解决需要在每个组件中添加css字符串的问题"><a href="#使用高阶组件解决需要在每个组件中添加css字符串的问题" class="headerlink" title="使用高阶组件解决需要在每个组件中添加css字符串的问题"></a>使用高阶组件解决需要在每个组件中添加<code>css</code>字符串的问题</h3><p>在每个组件中都需要把 css 字符串添加到 context 对象中很麻烦，可以写一个高阶组件，来解决这个问题。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> (DecoratedComponent, styles) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="class"><span class="keyword">class</span> <span class="title">newComponent</span> <span class="keyword">extends</span> <span class="title">React</span>() </span>&#123;</span><br><span class="line">    componentDidMount() &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; staticContext &#125; = <span class="keyword">this</span>.props;</span><br><span class="line">      <span class="keyword">if</span> (staticContext) &#123;</span><br><span class="line">        staticContext.css.push(styles._getCss());</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    render() &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">DecoratedComponent</span> &#123;<span class="attr">...this.props</span>&#125; /&gt;</span>;</span></span><br><span class="line"><span class="xml">    &#125;</span></span><br><span class="line"><span class="xml">  &#125;;</span></span><br><span class="line"><span class="xml">&#125;;</span></span><br></pre></td></tr></table></figure>

<h3 id="SEO"><a href="#SEO" class="headerlink" title="SEO"></a>SEO</h3><p>title 和 Description 主要是为了提高网站的点击率，对<code>SEO</code>意义不大。</p>
<h3 id="使用react-helmet定制-title-和-Description"><a href="#使用react-helmet定制-title-和-Description" class="headerlink" title="使用react-helmet定制 title 和 Description"></a>使用<code>react-helmet</code>定制 title 和 Description</h3><p><code>react-helmet</code>不仅可以帮助动态生成<code>title</code>和<code>description</code>,并且可以在服务端渲染时引用，让服务端渲染也是动态生成<code>title</code>和<code>description</code>。<br>在每个组件中使用<code>helmet</code>，则当路由到这个组件时，<code>title</code> 和 <code>description</code> 就自动发生了变化。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 拿到客户端渲染定义的helmet</span></span><br><span class="line"><span class="keyword">const</span> helmet = Helmet.renderStatic();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在html字符串中使用</span></span><br><span class="line"><span class="keyword">const</span> html = <span class="string">`</span></span><br><span class="line"><span class="string">    &lt;head&gt;</span></span><br><span class="line"><span class="string">        <span class="subst">$&#123;helmet.title.toString()&#125;</span></span></span><br><span class="line"><span class="string">        <span class="subst">$&#123;helmet.meta.toString()&#125;</span></span></span><br><span class="line"><span class="string">    &lt;/head&gt;</span></span><br><span class="line"><span class="string">        `</span>;</span><br></pre></td></tr></table></figure>

<h3 id="预渲染"><a href="#预渲染" class="headerlink" title="预渲染"></a>预渲染</h3><p>当对首屏时间要求不高，但是对 SEO 要求较高时，可以使用预渲染方案来解决这个问题。<br>预渲染就是当访问的是人时，就是正常的客户端渲染，但当访问的是爬虫蜘蛛时，则走预渲染服务器，预渲染服务器去取项目的页面，把页面的<code>dom</code>结构生成后再返回给蜘蛛。<br>预渲染服务器是如何获取页面的 dom 结构呢？<br>其实就是把页面上生成的 html 获取过来，就像我们使用开发者工具查看 html 结构一样，预渲染服务器就是拿的这个 html 字符串。</p>
<h3 id="搭建预渲染服务器"><a href="#搭建预渲染服务器" class="headerlink" title="搭建预渲染服务器"></a>搭建预渲染服务器</h3><p>预渲染需要预渲染服务，蜘蛛访问的是预渲染服务器，不是我们直接的网站页面。<br>预渲染服务器需要用到<code>prerender</code>，这个插件可以帮助我们轻松搭建预渲染服务器。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install prerender</span><br></pre></td></tr></table></figure>

<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> prerender = <span class="built_in">require</span>(<span class="string">"prerender"</span>);</span><br><span class="line"><span class="keyword">const</span> server = prerender(&#123; <span class="attr">port</span>: <span class="number">3200</span> &#125;);</span><br><span class="line">server.start();</span><br></pre></td></tr></table></figure>

<p>访问预渲染服务器的时候需要通过以下方式:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:3200/render?url=https://www.example.com/</span><br></pre></td></tr></table></figure>

<p>请求参数<code>url</code>的内容就是要抓取的页面。</p>
<h3 id="PreRender-原理"><a href="#PreRender-原理" class="headerlink" title="PreRender 原理"></a>PreRender 原理</h3><p>当访问预渲染服务器时，预渲染服务器会创建一个小浏览器，然后在这个小浏览器中访问 url 指定的页面，然后再在这个小浏览器中拿到<code>html</code>的内容，拿到后关掉这个小浏览器，把内容返回给蜘蛛。<br>由于这个过程，耗时可能比较长，但是蜘蛛只关心内容，不太关心响应时间，所以问题不大。</p>
<blockquote>
<p>预渲染参考网站 <a href="https://prerender.io/" target="_blank" rel="noopener">https://prerender.io/</a><br>这个网站详细讲解了预渲染原理和使用方式，并提供了便捷的的使用接口。</p>
</blockquote>
<h3 id="如何判断是否是蜘蛛在访问"><a href="#如何判断是否是蜘蛛在访问" class="headerlink" title="如何判断是否是蜘蛛在访问"></a>如何判断是否是蜘蛛在访问</h3><p>可以通过 nigix 进行判断，然后根据结果路由到不同的服务器上，如果是机器蜘蛛则路由到预渲染服务器，如果是客户在访问则路由到网站服务器</p>
</div></article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">文章作者: </span><span class="post-copyright-info"><a href="mailto:undefined">wenmu</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">文章链接: </span><span class="post-copyright-info"><a href="http://blog.wangpengpeng.site/2020/01/09/react-ssr%E7%AC%94%E8%AE%B0/">http://blog.wangpengpeng.site/2020/01/09/react-ssr%E7%AC%94%E8%AE%B0/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noopener">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="http://blog.wangpengpeng.site">温木的博客</a>！</span></div></div><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/ssr/">ssr</a><a class="post-meta__tags" href="/tags/react/">react</a></div><div class="post-qr-code"><div class="post-qr-code-item"><img class="post-qr-code__img" src="https://i.loli.net/2020/02/27/hOBzIxFDJMCrmtN.jpg"><div class="post-qr-code__desc">微信打赏</div></div></div><nav id="pagination"><div class="prev-post pull-left"><a href="/2020/01/09/React%E4%BA%8B%E4%BB%B6%E6%9C%BA%E5%88%B6/"><i class="fa fa-chevron-left">  </i><span>React事件机制</span></a></div><div class="next-post pull-right"><a href="/2020/01/09/OAuth%E8%AE%A4%E8%AF%81/"><span>OAuth认证</span><i class="fa fa-chevron-right"></i></a></div></nav><div id="vcomment"></div><script src="https://cdn1.lncld.net/static/js/3.0.4/av-min.js"></script><script src="https://cdn.jsdelivr.net/npm/valine/dist/Valine.min.js"></script><script>var notify = 'false' == true ? true : false;
var verify = 'false' == true ? true : false;
var GUEST_INFO = ['nick','mail','link'];
var guest_info = 'nick,mail,link'.split(',').filter(function(item){
  return GUEST_INFO.indexOf(item) > -1
});
guest_info = guest_info.length == 0 ? GUEST_INFO :guest_info;
window.valine = new Valine({
  el:'#vcomment',
  notify:notify,
  verify:verify,
  appId:'U1QqpeQNuBnRsfygHk1GQ30q-gzGzoHsz',
  appKey:'dunDQzEseDzgsKgw4qLQcog1',
  placeholder:'支持markdown！',
  avatar:'wavatar',
  guest_info:guest_info,
  pageSize:'20',
  lang: 'zh-cn'
})</script></div></div><footer><div class="layout" id="footer"><div class="copyright">&copy;2019 - 2020 By wenmu</div><div class="framework-info"><span>驱动 - </span><a href="http://hexo.io" target="_blank" rel="noopener"><span>Hexo</span></a><span class="footer-separator">|</span><span>主题 - </span><a href="https://github.com/Molunerfinn/hexo-theme-melody" target="_blank" rel="noopener"><span>Melody</span></a></div><div class="busuanzi"><script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script><span id="busuanzi_container_page_pv">该文访问量<span id="busuanzi_value_page_pv"></span><span></span></span></div></div></footer><i class="fa fa-arrow-up" id="go-up" aria-hidden="true"></i><script src="https://cdn.jsdelivr.net/npm/animejs@latest/anime.min.js"></script><script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@latest/dist/jquery.fancybox.min.js"></script><script src="https://cdn.jsdelivr.net/npm/velocity-animate@latest/velocity.min.js"></script><script src="https://cdn.jsdelivr.net/npm/velocity-ui-pack@latest/velocity.ui.min.js"></script><script src="/js/utils.js?version=1.6.1"></script><script src="/js/fancybox.js?version=1.6.1"></script><script src="/js/sidebar.js?version=1.6.1"></script><script src="/js/copy.js?version=1.6.1"></script><script src="/js/fireworks.js?version=1.6.1"></script><script src="/js/transition.js?version=1.6.1"></script><script src="/js/scroll.js?version=1.6.1"></script><script src="/js/head.js?version=1.6.1"></script><script>if(/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)) {
  $('#nav').addClass('is-mobile')
  $('footer').addClass('is-mobile')
}</script></body></html>